﻿using System.ComponentModel;
using Dyao.Book.Application.Abstractions;
using Dyao.Book.Contract;
using Dyao.Book.Core;
using Dyao.Book.Entity;
using Dyao.Book.Repository.Abstractions;
using Foundation.Core;
using Foundation.Spider;
using Foundation.Spider.DataFlow;
using Foundation.Spider.DataFlow.Parser;
using Foundation.Spider.Http;
using Foundation.Spider.Selector;
using Foundation.Uow;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Dyao.Book.Application
{
    [DisplayName("MatchBookSource")]
    public class MatchBookSourceSpider : Spider, IMatchBookSourceSpider
    {
        private readonly ISpiderManager _spiderMgr;

        private readonly IJsonSerializer _serializer;

        private readonly IBookSourceWarehouseManager _warehouseManager;

        private readonly DependenceServices _dependenceServices;

        public MatchBookSourceSpider(IOptions<SpiderOptions> options,
        DependenceServices services,
        ILogger<MatchBookSourceSpider> logger,
        ISpiderManager spiderMgr,
        IJsonSerializer serializer,
        IBookSourceWarehouseManager warehouseManager,
        DependenceServices dependenceServices) : base(options, services, logger)
        {
            _spiderMgr = spiderMgr;
            _serializer = serializer;
            _warehouseManager = warehouseManager;
            _dependenceServices = dependenceServices;
        }

        /// <inheritdoc cref="IMatchBookSourceSpider.CrawBookSourceUrlAsync"/>
        public async Task<bool> CrawBookSourceUrlAsync(CrawBookSourceDto sourceDto, CancellationToken cancellationToken)
        {
            var settings = new List<BookSourceWarehouseDto>();
            var uowMgr = _dependenceServices.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
            using (var uow = uowMgr.Begin(isReadDb: true))
            {
                settings = await _warehouseManager.GetSourceWarehousesForCacheAsync(cancellationToken);
            }

            await ExecuteAsync(cancellationToken, () =>
            {
                var requests = new List<Request>();
                foreach (var setting in settings)
                {
                    var request = BuildRequest(sourceDto, setting);
                    requests.Add(request);
                }

                AddRequestsAsync(requests);
            });

            return true;
        }

        /// <summary>
        /// 构建请求
        /// </summary>
        /// <param name="dto">书籍信息</param>
        /// <param name="setting">书源配置</param>
        /// <returns></returns>
        public Request BuildRequest(CrawBookSourceDto dto, BookSourceWarehouseDto setting)
        {
            string url = string.Format(setting.SearchUrl, dto.BookName);
            var request = new Request(url)
            {
                // 请求超时10秒
                Timeout = 10000
            };

            if (!setting.Header.IsNullOrEmpty())
            {
                var header = _serializer.Deserialize<BookSourceWarehouseHeaderDto>(setting.Header);
                if (!header.UserAgent.IsNullOrEmpty())
                    request.Headers.UserAgent = header.UserAgent;

                if (!header.Cookies.IsNullOrEmpty())
                    request.Headers.Cookie = header.Cookies;
            }

            request.Properties.Add(QiDianConstant.BookSourceWarehouseRequestProp, dto);
            request.Properties.Add(QiDianConstant.BookSourceWarehouseSettingRequestProp, setting);

            return request;
        }

        protected override async Task InitializeAsync(CancellationToken stoppingToken = default)
        {
            // 添加自定义解析
            AddDataFlow<MatchBookSourceDataParser>();
            AddDataFlow<ConsoleStorage>();

            // 错误信息
            OnRequestError += (request, response) =>
            {
                var bookProp = (CrawBookSourceDto)request.Properties.GetOrDefault(QiDianConstant.BookSourceWarehouseRequestProp);
                string message = $"Uri:{request.RequestUri}, ReasonPhrase:{response.ReasonPhrase}";
                _spiderMgr.PushMessageAsync(bookProp.BookId, OperationTypeEnum.BookSourceWarehouse, message);
            };

            // 超时请求
            OnRequestTimeout += (requests) =>
            {
                foreach (var request in requests)
                {
                    var bookProp = (CrawBookSourceDto)request.Properties.GetOrDefault(QiDianConstant.BookSourceWarehouseRequestProp);
                    string message = $"Uri:{request.RequestUri} RequestTimeout";
                    _spiderMgr.PushMessageAsync(bookProp.BookId, OperationTypeEnum.BookSourceWarehouse, message);
                }
            };
            await Task.CompletedTask;
        }


        public class MatchBookSourceDataParser : DataParser
        {
            public override Task InitializeAsync()
            {
                return Task.CompletedTask;
            }

            protected override async Task ParseAsync(DataFlowContext context)
            {
                var logger = context.ServiceProvider.GetService<ILogger<MatchBookSourceDataParser>>();
                var bookProp = (CrawBookSourceDto)context.Request.Properties.GetOrDefault(QiDianConstant.BookSourceWarehouseRequestProp);
                var settingProp = (BookSourceWarehouseDto)context.Request.Properties.GetOrDefault(QiDianConstant.BookSourceWarehouseSettingRequestProp);
                var serializer = context.ServiceProvider.GetService<IJsonSerializer>();
                var spiderRule = serializer.Deserialize<BookSourceWarehouseSpiderRuleDto>(settingProp.SpiderRule); // 规则
                var spiderMgr = context.ServiceProvider.GetService<ISpiderManager>();
                try
                {
                    // 网页数据解析
                    var masterNode = context.Selectable.Select(Selectors.XPath(spiderRule.BookSpiderRule.BookList));
                    var items = masterNode.SelectList(Selectors.XPath(spiderRule.BookSpiderRule.BookItem));
                    int index = 0;
                    foreach (var item in items)
                    {
                        if (index == 0 && spiderRule.SkipFirstLine)
                        {
                            index++;
                            continue;
                        }
                        var bookName = item.Select(Selectors.XPath(spiderRule.BookSpiderRule.BookName))?.Value;
                        var bookUrl = item.Select(Selectors.XPath(spiderRule.BookSpiderRule.BookUrl))?.Value;
                        var author = item.Select(Selectors.XPath(spiderRule.BookSpiderRule.Author))?.Value;

                        if (bookProp.BookName.Equals(bookName) && bookProp.Author.Equals(author))
                        {
                            var bookSource = new BookMatchSourceEntity()
                            {
                                BookId = bookProp.BookId,
                                CreateDateTime = DateTime.Now,
                                SourceKey = settingProp.SourceKey,
                                BookUrl = bookUrl,
                            };
                            await AddSourceAsync(context, bookSource);
                            break;
                        }
                        index++;
                    }
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, ex.Message);
                    await spiderMgr.PushMessageAsync(bookProp.BookId, OperationTypeEnum.BookSourceWarehouse, $"Parse Error:{ex.GetTraceMessage()}");
                }
            }

            /// <summary>
            /// 加入源
            /// </summary>
            /// <param name="context">上下文</param>
            /// <param name="sourceEntity">源</param>
            /// <returns></returns>
            public async Task AddSourceAsync(DataFlowContext context, BookMatchSourceEntity sourceEntity)
            {
                CancellationToken cancellationToken = default;
                //// 是否可达 
                //bool isReachable = await NetExtensions.IsUrlReachableAsync(sourceEntity.BookUrl);
                //if (!isReachable) throw new BusinessException($"Url:{sourceEntity.BookUrl} 不可访问.");

                var settingProp = (BookSourceWarehouseDto)context.Request.Properties.GetOrDefault(QiDianConstant.BookSourceWarehouseSettingRequestProp);

                var uowMgr = context.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
                var bookSourceRepo = context.ServiceProvider.GetRequiredService<IBookMatchSourceRepository>();
                var commonMgr = context.ServiceProvider.GetRequiredService<ICommonManager>();
                var bookRepo = context.ServiceProvider.GetRequiredService<IBookRepository>();
                using (var uow = uowMgr.Begin(isTransactional: true))
                {
                    var bookSource = await bookSourceRepo.GetEntityByBookIdAsync(sourceEntity.BookId, sourceEntity.SourceKey, cancellationToken);
                    if (bookSource == null)
                    {
                        try
                        {
                            var bookEntity = await bookRepo.GetByIdAsync(sourceEntity.BookId, cancellationToken);
                            bookEntity.IsMatchSource = true;
                            var log = new OperationLogDto()
                            {
                                SourceId = sourceEntity.BookId,
                                OperationType = OperationTypeEnum.Book,
                                OperationLog = $"匹配[{settingProp.Name}]书源地址:{sourceEntity.BookUrl}"
                            };
                            await bookSourceRepo.InsertAsync(sourceEntity, false, cancellationToken);
                            await commonMgr.AddOperationLogsAsync(log);
                            await uow.SaveChangesAsync();
                            await uow.CompleteAsync(cancellationToken);
                        }
                        catch (Exception)
                        {
                            await uow.RollbackAsync(cancellationToken);
                            throw;
                        }
                    }
                }
            }
        }
    }
}